#!/usr/bin/env ruby

# = Summary
#
# *mrnginx* (_mongrel_cluster-rails-nginx_ or _Mr Engine X_) is a helper
# script which makes configuring Rails sites with mongrel_cluster and nginx
# ridiculously easy.
#
# Here's what it does:
#
#   - creates mongrel_cluster YAML configuration file
#   - symlinks this file to /etc/mongrel_cluster
#   - creates nginx vhost file
#   - *WARNING*: overwrites nginx configuration file (/etc/nginx/nginx.conf)
#   - restarts mongrel_cluster
#   - restarts nginx
#
# It is currently designed to work with Ubuntu Server 7.10. Feel free to adapt
# it for other systems.
#
# mrnginx -h for more info.
#
# = Example usage
#
# cd /var/www/apps/myrailsapp
# mrnginx -s myrailsapp.com
#
# That's it! (Except DNS configuration of course).
#
# = Author
#
# Zubin Henner

require 'rdoc/usage'
require 'optparse'
require 'ostruct'
require 'fileutils'
require 'find'

class Mrnginx
  def self.version
    [0,1]
  end

  def self.new(args)
    # parse arguments
    @options = parse(args)
    default_options.each do |k,v|
      eval "@options.#{k} ||= v"
    end
    @options.port ||= next_usable_port || default_port
    # puts @options.inspect

    unless valid_rails_app?
      puts "#{@options.chdir} does not appear to be a valid rails app"
      exit
    end

    exit unless confirm_proceeding?

    # create mongrel_cluster conf
    puts "--\nCreating mongrel_cluster configuration file..."
    create_mongrel_cluster_conf_file

    # symlink mongrel_cluster conf file
    puts "--\nSymlinking mongrel_cluster configuration file..."
    exec "ln -sf #{mongrel_cluster_conf_file_path} /etc/mongrel_cluster/#{rails_app_name}.yml"

    # create nginx vhost file
    puts "--\nCreating nginx virtual host file..."
    create_nginx_vhost_file

    # set permissions for mongrel_cluster.yml
    exec "chown #{@options.user}:#{@options.group} #{mongrel_cluster_conf_file_path}"

    # stop mongrel, nginx
    exec "/etc/init.d/mongrel_cluster stop"
    exec "killall mongrel_rails"
    exec "/etc/init.d/nginx stop"

    # write nginx.conf
    write_nginx_conf_file

    # start mongrel_cluster, nginx
    exec "/etc/init.d/mongrel_cluster start"
    exec "/etc/init.d/nginx start"

    puts "--\nComplete!"
  end

  def self.confirm_proceeding?
    puts <<-EOS

      The following will be done:
        - create mongrel_cluster file: #{mongrel_cluster_conf_file_path}
        - symlink to /etc/mongrel_cluster/#{rails_app_name}.yml
        - create nginx vhost file: #{nginx_vhost_file_path}
        - stop mongrel_cluster
        - kill any lingering mongrel_rails processes
        - stop nginx
        - overwrite nginx configuration file (/etc/nginx/nginx.conf)
          (will be backed up to /etc/nginx/nginx.conf.bak if contents differ)
        - start mongrel_cluster
        - start nginx
    EOS
    print "\nProceed? [y/n] "
    continue = STDIN.getc.chr.downcase
    continue == "y"
  end

  def self.parse(args)
    options = OpenStruct.new

    opts = OptionParser.new do |opts|
      opts.banner = "Usage: mrnginx [options]"
      opts.separator ""
      opts.separator "Specific options:"

      opts.on(
        "-e", "--environment ENV", String, "Rails environment to run as",
        "[default #{default_options[:environment]}]"
      ) do |opt|
        options.environment = opt
      end

      opts.on(
        "-p", "--port PORT", Integer,
        "Starting port to bind as",
        "[default next unused port after 8000 + 10]"
      ) do |opt|
        options.port = opt
      end

      opts.on(
        "-a", "--address ADDR", String,
        "Address to bind to",
        "[default #{default_options[:address]}]"
      ) do |opt|
        options.address = opt
      end

      opts.on(
        "-c", "--chdir PATH", String,
        "Change to dir before starting (will be expanded)",
        "[default current working directory]"
      ) do |opt|
        options.chdir = opt.gsub(/\/$/, "") # strip trailing slash
      end

      opts.on(
        "-s", "--server-names DOMAIN1,DOMAIN2", String,
        "Domain(s) for virtual host",
        "[default #{default_options[:server_names].join(",")}]"
      ) do |opt|
        options.server_names = opt.split(",")
      end

      opts.on(
        "-N", "--num-servers INT", Integer,
        "Number of Mongrel servers",
        "[default #{default_options[:num_servers]}]"
      ) do |opt|
        options.num_servers = opt
      end

      opts.on(
        "--user", String,
        "User to run as.",
        "[default #{default_options[:user]}]"
      ) do |opt|
        options.user = opt
      end

      opts.on(
        "--group", String,
        "Group to run as.",
        "[default #{default_options[:group]}]"
      ) do |opt|
        options.user = opt
      end

      opts.separator ""
      opts.separator "Common options:"

      opts.on_tail("-h", "--help", "Show this message") do
        puts opts
        exit
      end

      opts.on_tail("-v", "--version", "Show version") do
        puts Mrnginx::version.join('.')
        exit
      end
    end
    opts.parse!(args)
    options
  rescue OptionParser::InvalidOption => e
    puts e, opts
    exit
  end

  def self.default_options
    {
      :address      => "127.0.0.1",
      :chdir        => `pwd`.strip,
      :environment  => "development",
      :group        => "www-data",
      :num_servers  => 3,
      :port         => nil, # set afterwards (needs @options.chdir)
      :server_names => %w(localhost),
      :user         => "www-data"
    }
  end

  def self.valid_rails_app?
    ! %w(app config public).detect { |dir|
      ! File.directory?("#{@options.chdir}/#{dir}")
    }
  end

  def self.default_port
    8000
  end

  def self.user
    `whoami`.strip
  end

  def self.exec(cmd)
    unless user == "root"
      cmd = "sudo #{cmd}"
    end
    puts cmd
    `#{cmd}`
  end

  # Scan vhost files for max port, then add 10
  def self.next_usable_port
    puts "Finding next usable port..."
    ports = []
    if File.directory?(vhost_path)
      Find.find(vhost_path) do |path|
        next if File.directory?(path)
        # skip previous version of vhost conf file
        next if path == nginx_vhost_file_path
        puts "  #{path}"
        ports += File.read(path).scan(/server [^\n]+:\d+;\n/).collect { |s|
          s.gsub(/^.*:(\d+);\n$/, "\\1").to_i
        }
      end
    end
    if ports.empty?
      puts "No ports in use, using default port #{default_port}"
      nil
    else
      puts "The following ports are in use: "+ports.sort.join(", ")
      ports.max + 10
    end
  end

  def self.vhost_path
    "/etc/nginx/vhosts"
  end

  def self.create_mongrel_cluster_conf_file
    cmd = [
      "mongrel_rails cluster::configure",
      "-e #{@options.environment}",
      "-p #{@options.port}",
      "-a #{@options.address}",
      "-c #{@options.chdir}",
      "-N #{@options.num_servers}",
      "--user #{@options.user}",
      "--group #{@options.group}"
    ].join(" ")
    puts cmd
    `cd #{@options.chdir}; #{cmd}`
  end

  def self.mongrel_cluster_conf_file_path
    "#{@options.chdir}/config/mongrel_cluster.yml"
  end

  def self.rails_app_name
    File.basename(@options.chdir)
  end

  def self.create_nginx_vhost_file
    fc = "upstream mongrel_#{rails_app_name} {\n"
    (1..@options.num_servers).each do |i|
      fc << "  server #{@options.address}:#{@options.port + i-1};\n"
    end
    fc << "}\n\n"
    fc << <<-EOS
    server {
      # port to listen on. Can also be set to an IP:PORT
      listen 80;

      # Set the max size for file uploads to 50Mb
      client_max_body_size 50M;

      # sets the domain[s] that this vhost server requests for
      server_name #{@options.server_names.join(" ")};

      # doc root
      root #{@options.chdir}/public;

      # vhost specific access log
      access_log  #{@options.chdir}/log/nginx.log  main;

      # this rewrites all the requests to the maintenance.html
      # page if it exists in the doc root. This is for capistrano's
      # disable web task
      if (-f $document_root/system/maintenance.html) {
        rewrite  ^(.*)$  /system/maintenance.html last;
        break;
      }

      location / {
        # needed to forward user's IP address to rails
        proxy_set_header  X-Real-IP  $remote_addr;

        # needed for HTTPS
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect false;
        proxy_max_temp_file_size 0;

        # If the file exists as a static file serve it directly without
        # running all the other rewite tests on it
        if (-f $request_filename) {
          break;
        }

        # check for index.html for directory index
        # if its there on the filesystem then rewite
        # the url to add /index.html to the end of it
        # and then break to send it to the next config rules.
        if (-f $request_filename/index.html) {
          rewrite (.*) $1/index.html break;
        }

        # this is the meat of the rails page caching config
        # it adds .html to the end of the url and then checks
        # the filesystem for that file. If it exists, then we
        # rewite the url to have explicit .html on the end
        # and then send it on its way to the next config rule.
        # if there is no file on the fs then it sets all the
        # necessary headers and proxies to our upstream mongrels
        if (-f $request_filename.html) {
          rewrite (.*) $1.html break;
        }

        if (!-f $request_filename) {
          # Use other cluster name here if you are running multiple
          # virtual hosts.
          proxy_pass http://mongrel_#{rails_app_name};
          break;
        }
      }

      error_page   500 502 503 504  /500.html;
      location = /500.html {
        root #{@options.chdir}/public;
      }
    }
    EOS
    # create vhosts dir if required
    FileUtils.mkdir_p(vhost_path)
    # write to file
    f = File.new(nginx_vhost_file_path, "w")
    f.write fc
    f.close
  end

  def self.nginx_vhost_file_path
    "#{vhost_path}/#{rails_app_name}.conf"
  end

  def self.write_nginx_conf_file
    nginx_conf_file_path = "/etc/nginx/nginx.conf"
    return if File.exists?(nginx_conf_file_path) &&
              File.read(nginx_conf_file_path) == nginx_conf_file_contents
    `cp -b #{nginx_conf_file_path} #{nginx_conf_file_path}.bak`
    f = File.new(nginx_conf_file_path, "w+")
    f.write(nginx_conf_file_contents)
    f.close
  end

  def self.nginx_conf_file_contents
    <<-EOS
    # =================================================================
    # = adapted from http://brainspl.at/nginx.conf.txt and railsjitsu =
    # =================================================================

    # user and group to run as
    user  www-data www-data;

    # number of nginx workers
    worker_processes  6;

    # pid of nginx master process
    pid /var/run/nginx.pid;

    # Number of worker connections. 1024 is a good default
    events {
      worker_connections 1024;
    }

    # start the http module where we config http access.
    http {
      # pull in mime-types. You can break out your config
      # into as many include's as you want to make it cleaner
      include /etc/nginx/mime.types;

      # set a default type for the rare situation that
      # nothing matches from the mimie-type include
      default_type  application/octet-stream;

      # configure log format
      log_format main '$remote_addr - $remote_user [$time_local] '
                      '"$request" $status  $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

      # main access log
      access_log  /var/log/nginx_access.log  main;

      # main error log
      error_log  /var/log/nginx_error.log debug;

      # no sendfile on OSX
      sendfile on;

      # These are good default values.
      tcp_nopush        on;
      tcp_nodelay       off;
      # output compression saves bandwidth
      gzip            on;
      gzip_http_version 1.0;
      gzip_comp_level 2;
      gzip_proxied any;
      gzip_types      text/plain text/html text/css application/x-javascript
                      text/xml application/xml application/xml+rss text/javascript;

      # virtual configs
      include /etc/nginx/vhosts/*.conf;
    }
    EOS
  end
end

Mrnginx.new(ARGV)
